/*
Copyright (C) 1997-2001 Id Software, Inc.

This program is free software; you can redistribute it and/or
modify it under the terms of the GNU General Public License
as published by the Free Software Foundation; either version 2
of the License, or (at your option) any later version.

This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.

See the GNU General Public License for more details.

You should have received a copy of the GNU General Public License
along with this program; if not, write to the Free Software
Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.

*/
/* Modifications
   Copyright 2003-2004 Bytonic Software
   Copyright 2010 Google Inc.
*/
package com.googlecode.playnquake.core.client;


import java.io.IOException;
import java.io.RandomAccessFile;
import java.util.Vector;

import com.googlecode.playnquake.core.common.*;
import com.googlecode.playnquake.core.game.Commands;
import com.googlecode.playnquake.core.util.Lib;

import static com.googlecode.playnquake.core.client.Keys.*;
/**
 * Key
 */
public class Key  {
	
	static int anykeydown = 0;
	static int key_waiting;
	static int history_line = 0;
	static boolean shift_down = false;
	static int[] key_repeats = new int[256];
	//static int[] keyshift = new int[256];
	static boolean[] menubound = new boolean[256];
	static boolean[] consolekeys = new boolean[256];

	// Move to Keys?
	static String[] keynames = new String[256];

	static {
		keynames[Keys.K_TAB] = "TAB";
		keynames[Keys.K_ENTER] = "ENTER";
		keynames[Keys.K_ESCAPE] = "ESCAPE";
		keynames[K_SPACE] = "SPACE";
		keynames[K_BACKSPACE] = "BACKSPACE";
		keynames[K_UPARROW] = "UPARROW";
		keynames[K_DOWNARROW] = "DOWNARROW";
		keynames[K_LEFTARROW] = "LEFTARROW";
		keynames[K_RIGHTARROW] = "RIGHTARROW";
		keynames[K_ALT] = "ALT";
		keynames[K_CTRL] = "CTRL";
		keynames[K_SHIFT] = "SHIFT";

		keynames[K_F1] = "F1";
		keynames[K_F2] = "F2";
		keynames[K_F3] = "F3";
		keynames[K_F4] = "F4";
		keynames[K_F5] = "F5";
		keynames[K_F6] = "F6";
		keynames[K_F7] = "F7";
		keynames[K_F8] = "F8";
		keynames[K_F9] = "F9";
		keynames[K_F10] = "F10";
		keynames[K_F11] = "F11";
		keynames[K_F12] = "F12";

		keynames[K_INS] = "INS";
		keynames[K_DEL] = "DEL";
		keynames[K_PGDN] = "PGDN";
		keynames[K_PGUP] = "PGUP";
		keynames[K_HOME] = "HOME";
		keynames[K_END] = "END";

		keynames[K_MOUSE1] = "MOUSE1";
		keynames[K_MOUSE2] = "MOUSE2";
		keynames[K_MOUSE3] = "MOUSE3";

		//	00092         {"JOY1", K_JOY1},
		//	00093         {"JOY2", K_JOY2},
		//	00094         {"JOY3", K_JOY3},
		//	00095         {"JOY4", K_JOY4},

		keynames[K_KP_HOME] = "KP_HOME";
		keynames[K_KP_UPARROW] = "KP_UPARROW";
		keynames[K_KP_PGUP] = "KP_PGUP";
		keynames[K_KP_LEFTARROW] = "KP_LEFTARROW";
		keynames[K_KP_5] = "KP_5";
		keynames[K_KP_RIGHTARROW] = "KP_RIGHTARROW";
		keynames[K_KP_END] = "KP_END";
		keynames[K_KP_DOWNARROW] = "KP_DOWNARROW";
		keynames[K_KP_PGDN] = "KP_PGDN";
		keynames[K_KP_ENTER] = "KP_ENTER";
		keynames[K_KP_INS] = "KP_INS";
		keynames[K_KP_DEL] = "KP_DEL";
		keynames[K_KP_SLASH] = "KP_SLASH";

		keynames[K_KP_PLUS] = "KP_PLUS";
		keynames[K_KP_MINUS] = "KP_MINUS";

		keynames[K_MWHEELUP] = "MWHEELUP";
		keynames[K_MWHEELDOWN] = "MWHEELDOWN";

		keynames[K_PAUSE] = "PAUSE";
		keynames[';'] = "SEMICOLON"; // because a raw semicolon seperates commands

		keynames[0] = "NULL";
	}

	/**
	 * 
	 */
	public static void Init() {
		for (int i = 0; i < 32; i++) {
			Globals.key_lines[i][0] = ']';
			Globals.key_lines[i][1] = 0;
		}
		Globals.key_linepos = 1;

		//
		// init ascii characters in console mode
		//
		for (int i = 32; i < 128; i++)
			consolekeys[i] = true;
		consolekeys[Keys.K_ENTER] = true;
		consolekeys[K_KP_ENTER] = true;
		consolekeys[Keys.K_TAB] = true;
		consolekeys[K_LEFTARROW] = true;
		consolekeys[K_KP_LEFTARROW] = true;
		consolekeys[K_RIGHTARROW] = true;
		consolekeys[K_KP_RIGHTARROW] = true;
		consolekeys[K_UPARROW] = true;
		consolekeys[K_KP_UPARROW] = true;
		consolekeys[K_DOWNARROW] = true;
		consolekeys[K_KP_DOWNARROW] = true;
		consolekeys[K_BACKSPACE] = true;
		consolekeys[K_HOME] = true;
		consolekeys[K_KP_HOME] = true;
		consolekeys[K_END] = true;
		consolekeys[K_KP_END] = true;
		consolekeys[K_PGUP] = true;
		consolekeys[K_KP_PGUP] = true;
		consolekeys[K_PGDN] = true;
		consolekeys[K_KP_PGDN] = true;
		consolekeys[K_SHIFT] = true;
		consolekeys[K_INS] = true;
		consolekeys[K_KP_INS] = true;
		consolekeys[K_KP_DEL] = true;
		consolekeys[K_KP_SLASH] = true;
		consolekeys[K_KP_PLUS] = true;
		consolekeys[K_KP_MINUS] = true;
		consolekeys[K_KP_5] = true;

		consolekeys['`'] = false;
		consolekeys['~'] = false;

//		for (int i = 0; i < 256; i++)
//			keyshift[i] = i;
//		for (int i = 'a'; i <= 'z'; i++)
//			keyshift[i] = i - 'a' + 'A';
//		keyshift['1'] = '!';
//		keyshift['2'] = '@';
//		keyshift['3'] = '#';
//		keyshift['4'] = '$';
//		keyshift['5'] = '%';
//		keyshift['6'] = '^';
//		keyshift['7'] = '&';
//		keyshift['8'] = '*';
//		keyshift['9'] = '(';
//		keyshift['0'] = ')';
//		keyshift['-'] = '_';
//		keyshift['='] = '+';
//		keyshift[','] = '<';
//		keyshift['.'] = '>';
//		keyshift['/'] = '?';
//		keyshift[';'] = ':';
//		keyshift['\''] = '"';
//		keyshift['['] = '{';
//		keyshift[']'] = '}';
//		keyshift['`'] = '~';
//		keyshift['\\'] = '|';

		menubound[Keys.K_ESCAPE] = true;
		for (int i = 0; i < 12; i++)
			menubound[K_F1 + i] = true;

		//
		// register our functions
		//
		Commands.addCommand("bind", Key.Bind_f);
		Commands.addCommand("unbind", Key.Unbind_f);
		Commands.addCommand("unbindall", Key.Unbindall_f);
		Commands.addCommand("bindlist", Key.Bindlist_f);
	}

	public static void ClearTyping() {
		Globals.key_lines[Globals.edit_line][1] = 0; // clear any typing
		Globals.key_linepos = 1;
	}

	/**
	 * Called by the system between frames for both key up and key down events.
	 */
	public static void Event(int key, boolean down, int time) {		
		String kb;
		String cmd;

		// hack for modal presses
		if (key_waiting == -1) {
			if (down)
				key_waiting = key;
			return;
		}

		// update auto-repeat status
		if (down) {
			key_repeats[key]++;
			if (key_repeats[key] > 1
				&& Globals.cls.key_dest == Constants.key_game
				&& !(Globals.cls.state == Constants.ca_disconnected))
				return; // ignore most autorepeats

			if (key >= 200 && Globals.keybindings[key] == null)
				Com.Printf(Key.KeynumToString(key) + " is unbound, hit F4 to set.\n");
		}
		else {
			key_repeats[key] = 0;
		}

		if (key == K_SHIFT)
			shift_down = down;

		// console key is hardcoded, so the user can never unbind it
		if (key == '`' || key == '~') {
			if (!down)
				return;

			Console.ToggleConsole_f.execute();
			return;
		}

		// any key during the attract mode will bring up the menu
		if (Globals.cl.attractloop && Globals.cls.key_dest != Constants.key_menu && !(key >= K_F1 && key <= K_F12))
			key = Keys.K_ESCAPE;

		// menu key is hardcoded, so the user can never unbind it
		if (key == Keys.K_ESCAPE) {
			if (!down)
				return;

			if (Globals.cl.frame.playerstate.stats[Constants.STAT_LAYOUTS] != 0 && Globals.cls.key_dest == Constants.key_game) {
				// put away help computer / inventory
				CommandBuffer.AddText("cmd putaway\n");
				return;
			}
			switch (Globals.cls.key_dest) {
				case Constants.key_message :
					Key.Message(key);
					break;
				case Constants.key_menu :
					Menu.Keydown(key);
					break;
				case Constants.key_game :
				case Constants.key_console :
					Menu.Menu_Main_f();
					break;
				default :
					Com.Error(Constants.ERR_FATAL, "Bad cls.key_dest");
			}
			return;
		}

		// track if any key is down for BUTTON_ANY
		Globals.keydown[key] = down;
		if (down) {
			if (key_repeats[key] == 1)
				Key.anykeydown++;
		}
		else {
			Key.anykeydown--;
			if (Key.anykeydown < 0)
				Key.anykeydown = 0;
		}

		//
		// key up events only generate commands if the game key binding is
		// a button command (leading + sign).  These will occur even in console mode,
		// to keep the character from continuing an action started before a console
		// switch.  Button commands include the kenum as a parameter, so multiple
		// downs can be matched with ups
		//
		if (!down) {
			kb = Globals.keybindings[key];
			if (kb != null && kb.length()>0 && kb.charAt(0) == '+') {
				cmd = "-" + kb.substring(1) + " " + key + " " + time + "\n";
				CommandBuffer.AddText(cmd);
			}
//			if (keyshift[key] != key) {
//				kb = Globals.keybindings[keyshift[key]];
//				if (kb != null && kb.length()>0 && kb.charAt(0) == '+') {
//					cmd = "-" + kb.substring(1) + " " + key + " " + time + "\n";
//					Cbuf.AddText(cmd);
//				}
//			}
			return;
		}

		//
		// if not a consolekey, send to the interpreter no matter what mode is
		//
		if ((Globals.cls.key_dest == Constants.key_menu && menubound[key])
			|| (Globals.cls.key_dest == Constants.key_console && !consolekeys[key])
			|| (Globals.cls.key_dest == Constants.key_game && (Globals.cls.state == Constants.ca_active || !consolekeys[key]))) {
			kb = Globals.keybindings[key];
			if (kb != null) {
				if (kb.length()>0 && kb.charAt(0) == '+') {
					// button commands add keynum and time as a parm
					cmd = kb + " " + key + " " + time + "\n";
					CommandBuffer.AddText(cmd);
				}
				else {
					CommandBuffer.AddText(kb + "\n");
				}
			}
			return;
		}

		if (!down)
			return; // other systems only care about key down events

//		if (shift_down)
//			key = keyshift[key];

		switch (Globals.cls.key_dest) {
			case Constants.key_message :
				Key.Message(key);
				break;
			case Constants.key_menu :
				Menu.Keydown(key);
				break;

			case Constants.key_game :
			case Constants.key_console :
				Key.Console(key);
				break;
			default :
				Com.Error(Constants.ERR_FATAL, "Bad cls.key_dest");
		}
	}

	/**
	 * Returns a string (either a single ascii char, or a K_* name) for the 
	 * given keynum.
	 */
	public static String KeynumToString(int keynum) {
		if (keynum < 0 || keynum > 255)
			return "<KEY NOT FOUND>";
		if (keynum > 32 && keynum < 127)
			return Character.toString((char) keynum);

		if (keynames[keynum] != null)
			return keynames[keynum];

		return "<UNKNOWN KEYNUM>";
	}

	/**
	 * Returns a key number to be used to index keybindings[] by looking at
	 * the given string. Single ascii characters return themselves, while
	 * the K_* names are matched up.
	 */
	static int StringToKeynum(String str) {

		if (str == null)
			return -1;

		if (str.length() == 1)
			return str.charAt(0);

		for (int i = 0; i < keynames.length; i++) {
			if (str.equalsIgnoreCase(keynames[i]))
				return i;
		}

		return -1;
	}

	public static void Message(int key) {

		if (key == Keys.K_ENTER || key == K_KP_ENTER) {
			if (Globals.chat_team)
				CommandBuffer.AddText("say_team \"");
			else
				CommandBuffer.AddText("say \"");

			CommandBuffer.AddText(Globals.chat_buffer);
			CommandBuffer.AddText("\"\n");

			Globals.cls.key_dest = Constants.key_game;
			Globals.chat_buffer = "";
			return;
		}

		if (key == Keys.K_ESCAPE) {
			Globals.cls.key_dest = Constants.key_game;
			Globals.chat_buffer = "";
			return;
		}

		if (key < 32 || key > 127)
			return; // non printable

		if (key == K_BACKSPACE) {
			if (Globals.chat_buffer.length() > 2) {
				Globals.chat_buffer = Globals.chat_buffer.substring(0, Globals.chat_buffer.length() - 2);
			}
			else
				Globals.chat_buffer = "";
			return;
		}

		if (Globals.chat_buffer.length() > Constants.MAXCMDLINE)
			return; // all full

		Globals.chat_buffer += (char) key;
	}

	/**
	 * Interactive line editing and console scrollback.
	 */
	public static void Console(int key) {

		switch (key) {
			case K_KP_SLASH :
				key = '/';
				break;
			case K_KP_MINUS :
				key = '-';
				break;
			case K_KP_PLUS :
				key = '+';
				break;
			case K_KP_HOME :
				key = '7';
				break;
			case K_KP_UPARROW :
				key = '8';
				break;
			case K_KP_PGUP :
				key = '9';
				break;
			case K_KP_LEFTARROW :
				key = '4';
				break;
			case K_KP_5 :
				key = '5';
				break;
			case K_KP_RIGHTARROW :
				key = '6';
				break;
			case K_KP_END :
				key = '1';
				break;
			case K_KP_DOWNARROW :
				key = '2';
				break;
			case K_KP_PGDN :
				key = '3';
				break;
			case K_KP_INS :
				key = '0';
				break;
			case K_KP_DEL :
				key = '.';
				break;
		}

		if (key == 'l') {
			if (Globals.keydown[K_CTRL]) {
				CommandBuffer.AddText("clear\n");
				return;
			}
		}

		if (key == Keys.K_ENTER || key == K_KP_ENTER) {
			// backslash text are commands, else chat
			if (Globals.key_lines[Globals.edit_line][1] == '\\' || Globals.key_lines[Globals.edit_line][1] == '/')
				CommandBuffer.AddText(
					Compatibility.newString(Globals.key_lines[Globals.edit_line], 2, Lib.strlen(Globals.key_lines[Globals.edit_line]) - 2));
			else
				CommandBuffer.AddText(
					Compatibility.newString(Globals.key_lines[Globals.edit_line], 1, Lib.strlen(Globals.key_lines[Globals.edit_line]) - 1));

			CommandBuffer.AddText("\n");

			Com.Printf(Compatibility.newString(Globals.key_lines[Globals.edit_line], 1, Lib.strlen(Globals.key_lines[Globals.edit_line]) - 1) + "\n");
			Globals.edit_line = (Globals.edit_line + 1) & 31;
			history_line = Globals.edit_line;
		
			Globals.key_lines[Globals.edit_line][0] = ']';
			Globals.key_linepos = 1;
			if (Globals.cls.state == Constants.ca_disconnected)
				Screen.UpdateScreen(); // force an update, because the command may take some time
			return;
		}

		if (key == Keys.K_TAB) {
			// command completion
			CompleteCommand();
			return;
		}

		if ((key == K_BACKSPACE) || (key == K_LEFTARROW) || (key == K_KP_LEFTARROW) || ((key == 'h') && (Globals.keydown[K_CTRL]))) {
			if (Globals.key_linepos > 1)
				Globals.key_linepos--;
			return;
		}

		if ((key == K_UPARROW) || (key == K_KP_UPARROW) || ((key == 'p') && Globals.keydown[K_CTRL])) {
			do {
				history_line = (history_line - 1) & 31;
			}
			while (history_line != Globals.edit_line && Globals.key_lines[history_line][1] == 0);
			if (history_line == Globals.edit_line)
				history_line = (Globals.edit_line + 1) & 31;
			//Lib.strcpy(Globals.key_lines[Globals.edit_line], Globals.key_lines[history_line]);
			System.arraycopy(Globals.key_lines[history_line], 0, Globals.key_lines[Globals.edit_line], 0, Globals.key_lines[Globals.edit_line].length);
			Globals.key_linepos = Lib.strlen(Globals.key_lines[Globals.edit_line]);
			return;
		}

		if ((key == K_DOWNARROW) || (key == K_KP_DOWNARROW) || ((key == 'n') && Globals.keydown[K_CTRL])) {
			if (history_line == Globals.edit_line)
				return;
			do {
				history_line = (history_line + 1) & 31;
			}
			while (history_line != Globals.edit_line && Globals.key_lines[history_line][1] == 0);
			if (history_line == Globals.edit_line) {
				Globals.key_lines[Globals.edit_line][0] = ']';
				Globals.key_linepos = 1;
			}
			else {
				//Lib.strcpy(Globals.key_lines[Globals.edit_line], Globals.key_lines[history_line]);
				System.arraycopy(Globals.key_lines[history_line], 0, Globals.key_lines[Globals.edit_line], 0, Globals.key_lines[Globals.edit_line].length);
				Globals.key_linepos = Lib.strlen(Globals.key_lines[Globals.edit_line]);
			}
			return;
		}

		if (key == K_PGUP || key == K_KP_PGUP) {
			Globals.con.display -= 2;
			return;
		}

		if (key == K_PGDN || key == K_KP_PGDN) {
			Globals.con.display += 2;
			if (Globals.con.display > Globals.con.current)
				Globals.con.display = Globals.con.current;
			return;
		}

		if (key == K_HOME || key == K_KP_HOME) {
			Globals.con.display = Globals.con.current - Globals.con.totallines + 10;
			return;
		}

		if (key == K_END || key == K_KP_END) {
			Globals.con.display = Globals.con.current;
			return;
		}

		if (key < 32 || key > 127)
			return; // non printable

		if (Globals.key_linepos < Constants.MAXCMDLINE - 1) {
			Globals.key_lines[Globals.edit_line][Globals.key_linepos] = (byte) key;
			Globals.key_linepos++;
			Globals.key_lines[Globals.edit_line][Globals.key_linepos] = 0;
		}

	}

	private static void printCompletions(String type, Vector compl) {
		Com.Printf(type);
		for (int i = 0; i < compl.size(); i++) {
			Com.Printf((String)compl.get(i) + " ");
		}
		Com.Printf("\n");
	}
	
	static void CompleteCommand() {
		
		int start = 1;
		if (Globals.key_lines[Globals.edit_line][start] == '\\' ||  Globals.key_lines[Globals.edit_line][start] == '/')
			start++;
		
		int end = start;
		while (Globals.key_lines[Globals.edit_line][end] != 0) end++;

		String s = Compatibility.newString(Globals.key_lines[Globals.edit_line], start, end-start);
		
		Vector cmds = Commands.CompleteCommand(s);
		Vector vars = ConsoleVariables.CompleteVariable(s);
		
		int c = cmds.size();
		int v = vars.size();
		
		if ((c + v) > 1) {
			if (c > 0) printCompletions("\nCommands:\n", cmds);
			if (v > 0) printCompletions("\nVariables:\n", vars);
			return;
		} else if (c == 1) {
			s = (String)cmds.get(0);
		} else if (v == 1) {
			s = (String)vars.get(0);
		} else return;
		
		Globals.key_lines[Globals.edit_line][1] = '/';
		byte[] bytes = Lib.stringToBytes(s);
		System.arraycopy(bytes, 0, Globals.key_lines[Globals.edit_line], 2, bytes.length);
		Globals.key_linepos = bytes.length + 2;
		Globals.key_lines[Globals.edit_line][Globals.key_linepos++] = ' ';
		Globals.key_lines[Globals.edit_line][Globals.key_linepos] = 0;
		
		return;
	}

	public static ExecutableCommand Bind_f = new ExecutableCommand() {
		public void execute() {
			Key_Bind_f();
		}
	};

	static void Key_Bind_f() {
		int c = Commands.Argc();

		if (c < 2) {
			Com.Printf("bind <key> [command] : attach a command to a key\n");
			return;
		}
		int b = StringToKeynum(Commands.Argv(1));
		if (b == -1) {
			Com.Printf("\"" + Commands.Argv(1) + "\" isn't a valid key\n");
			return;
		}

		if (c == 2) {
			if (Globals.keybindings[b] != null)
				Com.Printf("\"" + Commands.Argv(1) + "\" = \"" + Globals.keybindings[b] + "\"\n");
			else
				Com.Printf("\"" + Commands.Argv(1) + "\" is not bound\n");
			return;
		}

		// copy the rest of the command line
		String cmd = ""; // start out with a null string
		for (int i = 2; i < c; i++) {
			cmd += Commands.Argv(i);
			if (i != (c - 1))
				cmd += " ";
		}

		SetBinding(b, cmd);
	}

	static void SetBinding(int keynum, String binding) {
		if (keynum == -1)
			return;

		// free old bindings
		Globals.keybindings[keynum] = null;

		Globals.keybindings[keynum] = binding;
	}

	static ExecutableCommand Unbind_f = new ExecutableCommand() {
		public void execute() {
			Key_Unbind_f();
		}
	};

	static void Key_Unbind_f() {

		if (Commands.Argc() != 2) {
			Com.Printf("unbind <key> : remove commands from a key\n");
			return;
		}

		int b = Key.StringToKeynum(Commands.Argv(1));
		if (b == -1) {
			Com.Printf("\"" + Commands.Argv(1) + "\" isn't a valid key\n");
			return;
		}

		Key.SetBinding(b, null);
	}

	static ExecutableCommand Unbindall_f = new ExecutableCommand() {
		public void execute() {
			Key_Unbindall_f();
		}
	};

	static void Key_Unbindall_f() {
		for (int i = 0; i < 256; i++)
			Key.SetBinding(i, null);
	}

	static ExecutableCommand Bindlist_f = new ExecutableCommand() {
		public void execute() {
			Key_Bindlist_f();
		}
	};

	static void Key_Bindlist_f() {
		for (int i = 0; i < 256; i++)
			if (Globals.keybindings[i] != null && Globals.keybindings[i].length() != 0)
				Com.Printf(Key.KeynumToString(i) + " \"" + Globals.keybindings[i] + "\"\n");
	}

	static void ClearStates() {
		int i;

		Key.anykeydown = 0;

		for (i = 0; i < 256; i++) {
			if (Globals.keydown[i] || key_repeats[i]!=0)
				Event(i, false, 0);
			Globals.keydown[i] = false;
			key_repeats[i] = 0;
		}
	}

	public static void WriteBindings(RandomAccessFile f) {
		for (int i = 0; i < 256; i++)
			if (Globals.keybindings[i] != null && Globals.keybindings[i].length() > 0)
				try {
					f.writeBytes("bind " + KeynumToString(i) + " \"" + Globals.keybindings[i] + "\"\n");
				} catch (IOException e) {}
	}

}
